<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>策略模式</title>
</head>
<body>
    假如我们有一个需求 需要为不同绩效等级的员工计算不同的年终奖金
    <div style="position: absolute; background: blue">我是div</div>


    <form action="http://baidu.com/register" id="registerForm" method="post"> 
            请输入用户名:<input type="text" name="userName"/ >
            请输入密码:<input type="text" name="password"/ >
            请输入手机号码:<input type="text" name="phoneNumber"/ >
            <button>提交</button> 
    </form>

    <script>
        // 1. 最初的代码实现 
        // 该函数 接收两个参数 1 是工资数额  2 是绩效考核等级
        var calculateBonus = function(performanceLevel, salary) {
            if ( performanceLevel === 'S') return salary * 4;
            if ( performanceLevel === 'A') return salary * 3;
            if ( performanceLevel === 'C') return salary * 2;
        }
        var s = calculateBonus('S', 10000);
        console.log(s);
        // 虽然这么写起来非常简单 但是存在着明显的缺点
        // 1 包含了大量的 if语句 
        // 2 缺乏弹性  如果有一天需要增加一种绩效 或者修改一个绩效 那么必须深入 calculateBonus的内部实现
        // 3 算法的复用性差  如果程序的其他地方需要重用这些计算公式的呢？


        // 2. 使用组合函数重构代码
        // 我们把算法封装到一个个函数 
        var performanceLevelS = function(salary) {
            return salary * 4
        }
        var performanceLevelA = function(salary) {
            return salary * 3
        }

        var calculateBonus1  = function(performanceLevel, salary) {
            if (performanceLevel === 'S') return performanceLevelS(salary)
        }

        // 虽然得到一定的改善 但是我们依然没有解决一个问题 calculateBonus1 函数可能会越来越大 而且在系统变化的时候缺乏弹性

        // 3 使用策略模式重构代码
        // 经过思考 我们想到更好的解决办法 使用策略模式来重构代码
        // 策略模式指的是定义一系列的算法，把他们一个个封装起来。
        // 将不变的部分和变化的不封隔开是每个设计模式的主题，策略模式的目的就是将算法的使用与算法的实现分离开来

        // 在上面的例子中 算法的使用方式是不变的 都是根据某个算法取得计算后的奖金数额。
        // 而算法的实现是各异和变化的 每种绩效对应着不同的计算规则

        // 一个基于策略模式的程序至少由两部分组成。
        // 第一部分是 一组策略类        策略类封装了具体的算法 并负责具体的计算过程
        // 第二部分是 环境类Context    Context接受客户的请求 随后把请求委托给某一个策略类 要做到这点 说明context 中要维持对某个策略对象的引用
        

        // 模仿传统面向对象语言的实现
        // 先把每种绩效的计算规则都封装在对应的策略类里面
        var performanceS  = function() {};
        performanceS.prototype.calculate = function( salary ) {
            return salary * 4
        }

        var performanceA = function() {};
        performanceA.prototype.calculate = function( salary ) {
            return salary * 3
        }

        // 定义奖金类 calculateBonus
        var Bonus = function() {
            this.salary = null;         // 原始工资
            this.strategy = null;       // 绩效等级对应的策略对象
        }

        Bonus.prototype.setSalary = function( salary ) {
            this.salary = salary;       // 设置员工的原始工资
        } 
        Bonus.prototype.setStrategy = function( strategy ) {
            this.setSalary = strategy;  // 设置员工绩效等级对应的策略对象
        }

        Bonus.prototype.getBonus = function(strategy) { // 取得奖金数额
            return this.strategy.calculate(this.salary) // 把计算奖金的操作委托给对应的策略对象
        }
        
        // 定义一系列的算法 把他们一个个封装起来 并且使得他们可以相互替换
        // 详细: 定义一系列的算法 把他们各种封装成策略类 算法被封装在策略类内部的方法里。context总是把这些请求委托给这些策略对象中间的某一个进行计算

        // 5.2 JavaScript中的策略模式
        // 函数也是对象 更简单和直接的做法就是吧 strategy直接定义为函数
        var strategies = {
            "S": function(salary) {
                return salary * 4
            },
            "A": function(salary) {
                return salary * 3
            },
            "B": function( salary) {
                return salary * 2
            }
        }
        var calculateBonus = function(level, salary) {
            return strategies[level](salary)
        }
        console.log(calculateBonus('S', 20000));

        // 编写一个缓动的小球
        // 缓动算法 分别接受4个参数 动画已消耗的时间 小球的原始位置 小球的目标位置 动画持续的总时间
        var tween = {
            linear: function(t, b, c, d) {
                return c * t / d + b
            },
            easeIn: function(t, b, c, d) {
                return c * ( t /= d ) * t + b;
            },
            strongEaseIn: function(t, b, c, d){
                return c * ( t /= d ) * t * t * t * t + b;
            },
            strongEaseOut: function(t, b, c, d){
                return c * ( ( t = t / d - 1) * t * t * t * t + 1 ) + b
            },
            sineaseIn: function( t, b, c, d ){
                return c * ( t /= d) * t * t + b; 
            },
            sineaseOut: function(t,b,c,d){
                return c * ( ( t = t / d - 1) * t * t +1 ) + b
            }

        }

        var Animate = function(dom ){
            this.dom = dom;             // 进行运动的 dom 节点
            this.startTime = 0;         // 动画开始时间
            this.startPos = 0;          // 动画开始时，dom 节点的位置，即 dom 的初始位置 
            this.endPos = 0;            // 动画结束时，dom 节点的位置，即 dom 的目标位置
            this.propertyName = null;   // dom 节点需要被改变的 css 属性名
            this.easing = null;         // 缓动算法
            this.duration = null;       // 动画持续时间
        };

        // 5.6 表单校验
        // 假设我们在编写一个注册的页面 在点击注册按钮之前 有几条校验逻辑
        // 1 用户名不能为空
        // 2 密码长度不少于6位
        // 3 手机号码格式需要符合格式

        // 版本一
        // var registerForm = document.querySelector('#registerForm');

        // registerForm.addEventListener('submit', function (e) {
        //     e.preventDefault()
        //     if (registerForm.userName.value === '') {
        //         console.log('用户名不能为空');
        //         return false
        //     }
        //     if (registerForm.password.value.length < 6 ) {
        //         console.log('密码长度');
        //         return false
        //     }
        //     if ( !/(^1[3|5|8][0-9]{9}$)/.test( registerForm.phoneNumber.value )) {
        //         alert('手机号码格式不正确')
        //         return false
        //     }
        // })

        // 这是一种很常见的代码编写方式 他的缺点跟计算奖金最初版本一模一样 
        // 1 包含了大量 if-else 语句
        // 2 缺乏弹性 如果增加新的校验规则 或者想把密码的长度校验从5 改成 8 就都必须深入onsubmit 的内部 违反了开闭原则
        // 3 算法复用性差 如果程序中增加了另外一个表单 这个表单也需要进行类似的校验 那么很可能要将这些校验逻辑复制的漫天遍野
        
        // 5.6.2 用策略模式重构表单校验
        // 很显然第一步我们要把这些校验规则逻辑都封装成策略对象
        let strategies1 = {
            isNonEmpty: function(value, errorMsg) { // 不能为空
                if (value === '') {
                    return errorMsg;
                }
            },
            minLength: function(value, errorMsg) {
                if (value.length < length) {
                    return errorMsg
                }
            },
            isMobile: function(value, errorMsg) {
                if (value) return '不能为空'
                if (!/(^1[3|5|8][0-9]{9}$)/.test( value )) {
                    return errorMsg
                }
            }
        }



        // 实现Validator类 Validator 类在这里作为 Context 负责接收用户的请求 并委托给 strategy(策略) 对象

        // 我们先创建了一个validator对象 然后通过 add方法 往validator对象中添加一些校验规则 
        // validator add 接收 3 个参数
        // validator start 启动校验


        class Validator {
            constructor() {
                this.cache = []
            }
            add(dom, rule, errorMsg) {
                var ary = rule.split(':'); // 把strategy 和 参数分开
                this.cache.push( function () {
                    var strategy = ary.shift(); // 用户挑选的 strategy
                    ary.unshift( dom.value )    // 把input的value添加进参数列表
                    ary.push(errorMsg)
                    return strategies1[ strategy ].apply( dom, ary)
                })
            }
            start() {
                let errMsg = []
                for (var i = 0, validatorFunc; validatorFunc = this.cache[ i++ ];) {
                    var msg = validatorFunc()

                    if (msg) {
                         errMsg.push(msg)
                    }
                }
                return errMsg
            }
        }

        var validatorFunc = function() {
            var validator = new Validator();        // 创建一个validator类
            // 添加一些校验规则 
            validator.add( registerForm.userName, 'isNonEmpty', '用户名不能为空')
            validator.add( registerForm.password, 'minLength:6', '密码长度不能少于6位')
            validator.add( registerForm.phoneNumber, 'isMobile', '手机号码格式不正确')
            var errorMsg = validator.start();
            return errorMsg
        }

        var registerForm = document.querySelector('#registerForm');
        registerForm.addEventListener('submit', function(e) {
            e.preventDefault();
            var errorMsg = validatorFunc();
            if (errorMsg) {
                console.log(errorMsg);
                return false
            }
        })

        // 如果我们想要验证多个条件 比如 既想用户名不能为空 又不能长于10个字符串
        class Validator2 {
            constructor() {
                this.cache = []
            }
            add(dom, rules) {
                for (let i = 0, rule; rule = rules[ i++ ];) {
                    // 验证策略
                    var strategyAry = rule.strategy.split(':');
                    // 当前错误信息
                    var errMsg = rule.errorMsg;
                    this.cache.push( function(){
                        var strategy = strategyAry.shift(); // 截取第一个
                        strategyAry.unshift( dom.value )
                        strategyAry.push( errorMsg );
                        return strategies1[ strategy ].apply( dom, strategyAry )
                    })
                }
            }
            start() {
                for (let i = 0, validatorFunc; validatorFunc = this.cache[i ++ ];) {
                    var errorMsg = validatorFunc()
                    if (errorMsg ) return errorMsg
                }
            }
        }

        // 客户端调用
        // validator2.add(registerForm.userName, [{
        //     strategy: 'isNonEmpty',
        //     errorMsg: '用户名不能为空'
        // }])




    </script>


</body>
</html>